package com.ms.jsf.omnifaces;

import javax.faces.context.FacesContext;
import java.util.HashMap;
import java.util.Map;


/**
 * This class helps in letting code run within its own scope. Such scope is defined by specific variables being
 * available to EL within it. The request scope is used to store the variables.
 *
 * @author Arjan Tijms
 */
public class ScopedRunner {

    FacesContext context;
    Map<String, Object> scopedVariables;
    Map<String, Object> previousVariables = new HashMap<String, Object>();

    public ScopedRunner(FacesContext context) {
        this(context, new HashMap<String, Object>());
    }

    public ScopedRunner(FacesContext context, Map<String, Object> scopedVariables) {
        this.context = context;
        this.scopedVariables = scopedVariables;
    }

    /**
     * Adds the given variable to this instance. Can be used in a builder-pattern.
     *
     * @param key   the key name of the variable
     * @param value the value of the variable
     * @return this ScopedRunner, so adding variables and finally calling invoke can be chained.
     */
    public ScopedRunner with(String key, Object value) {
        scopedVariables.put(key, value);
        return this;
    }

    /**
     * Invokes the callback within the scope of the variables being given in the constructor.
     *
     * @param callback
     */
    public void invoke(Callback.Void callback) {
        try {
            setNewScope();
            callback.invoke();
        } finally {
            restorePreviousScope();
        }
    }

    private void setNewScope() {
        previousVariables.clear();

        Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
        for (Map.Entry<String, Object> entry : scopedVariables.entrySet()) {
            Object previousVariable = requestMap.put(entry.getKey(), entry.getValue());
            if (previousVariable != null) {
                previousVariables.put(entry.getKey(), previousVariable);
            }
        }
    }

    private void restorePreviousScope() {
        try {
            Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
            for (Map.Entry<String, Object> entry : scopedVariables.entrySet()) {
                Object previousVariable = previousVariables.get(entry.getKey());
                if (previousVariable != null) {
                    requestMap.put(entry.getKey(), previousVariable);
                } else {
                    requestMap.remove(entry.getKey());
                }
            }
        } finally {
            previousVariables.clear();
        }
    }

}
